#!/usr/bin/env ruby

# This program scans a set of data from a MAG stripe and looks for pieces of
# a provided serial number in the contents. 
# Additionally, it searches for the existance of a monetary value stored in 
# the data.

def usage 
    $stderr.puts "USAGE: #{File.basename($0)} <card file> <sn>"
    $stderr.puts "-"*40
    $stderr.puts " card file     The file containing the raw scanned data"
    $stderr.puts " sn            The serial number from the card itself"
    $stderr.puts
    exit 42
end

TRACK1_FS='^'
TRACK2_FS='='
TRACK1_BEGIN='%'
TRACK2_BEGIN=';'
TRACK_END='?'

class Track
    attr_reader :fields
    def initialize(format, data)
        @format, @data = format, data
        @fields = []
        @fs = format.nil? ? TRACK2_FS : TRACK1_FS
        parse_fields
    end
    def parse_fields
        @data.split(@fs).each do |field|
            next if field.empty?
            @fields << field
        end
    end
end

def chunk(str,size)
    str.scan(/.{1,#{size}}/)
end

def has_card_value data
    data =~ /\$\d+\$/
end

def get_value data
    if data =~ /\$(\d+)\$/
        return $1.to_i / 100.0
    end
    return 0.0
end

# The encoding on some cards (credt cards in particular) use the LUHN algorithm
# when generating the account numbers. Thi scheck will identify candidate 
# LUHN-produced account numbers
# Reference: http://en.wikipedia.org/wiki/Luhn_algorithm
def luhn_candidate f
    odds = []
    evens = []
    check = 0
    fr = f.reverse.chars
    fr.each_with_index do |c, i|
        if i == 0
            check = c.to_i
            next
        end
        odds << c.to_i if i.odd?
        evens << c.to_i if i.even?
    end
    sum = 0
    evens.each { |e| sum += e }
    odds.each do |o|
        d = o * 2
        d = (d - 9) if d >= 10
        sum += d
    end
    sum += check
    sum % 10 == 0
end

# Given a buffer of all card data, split it into the three tracks
# For now, the third track is always returned as nil
def split_tracks cdata
    t1, t2, t3 = nil, nil, nil
    if cdata[0..0] != TRACK1_BEGIN
        $stderr.puts "Invalid start sentinel: '%s'. Abort" % [cdata[0..0]]
        exit 1
    end
    format = cdata[1..1]
    cdata = cdata[2..-1]
    i = cdata.index(TRACK_END)
    if i.nil?
        $stderr.puts "Track separator not found. Assuming single track"
        t1 = Track.new(format, cdata)
    else
        cdata1 = cdata[0...i]
        cdata2 = cdata[(i+1)..-1]
        i = cdata2.index(TRACK2_BEGIN)
        if i.nil?
            $stderr.puts "Track 2 start sentinel not found. Abort"
            exit 1
        end
        cdata2 = cdata2[(i+1)..-1]
        i = cdata2.index(TRACK_END)
        if (i.nil?)
            $stderr.puts "Track 2 end sentinel not found. Abort"
            exit 1
        end
        cdata2 = cdata2[0...i]
        t1 = Track.new(format,cdata1)
        t2 = Track.new(nil, cdata2)
    end
    [t1,t2,t3]
end

cfile = ARGV.shift || usage
sn = ARGV.shift || usage

begin
    data = File.new(cfile,"rb").read
rescue
    $stderr.puts "Unable to read card file: #{cfile}. Aborting"
    exit 1
end

track1, track2, track3 = split_tracks data
puts "Track 1 Fields:"
track1.fields.each_with_index do |f, i|
    puts " (%02d) %s" % [i,f]
end
puts
puts "Track 2 Fields:"
track2.fields.each_with_index do |f, i|
    puts " (%02d) %s" % [i,f]
end

puts
track1.fields.each do |f|
    if has_card_value f
        puts "Track 1 looks to have an initial card balance"
        puts "  --> %s <--" % [f]
        puts "  Estimating initial card balance of $%0.2f" % [get_value(f)]
    end
end
track2.fields.each do |f|
    if has_card_value f
        puts "Track 2 looks to have an initial card balance"
        puts "  --> %s <--" % [f]
        puts "  Estimating initial card balance of $%0.2f" % [get_value(f)]
    end
end

puts
puts "Checking SN: %s %s" % [sn, luhn_candidate(sn) ? "(luhn)" : ""]
puts

size = sn.size

# We only split SN down to chunks of 4 before bailing
# Any more and we run the risk of higher false positive values
found = false
while size >= 4
    if found
        size = 0
        next
    end
    puts "Checking chunks of size: #{size}"
    track1.fields.each_with_index do |f,i|
        # TODO: We should really consider the case where we get something like
        # 123456789 and 456789 exists in the data. Our current chunking will
        # not consider that case it only chunks from the 'front' of the number
        # so we would only ever check 123456 in this case.
        chunk(sn,size).each do |bit|
            next unless bit.size >= size
            if f.index bit
                puts "   Found #{bit} in field #{i} of track 1"
                puts "    --> %s <-- " % [f]
                found = true
            end
        end
    end
    track2.fields.each_with_index do |f,i|
        chunk(sn,size).each do |bit|
            next unless bit.size >= size
            if f.index bit
                puts "   Found #{bit} in field #{i} of track 2"
                puts "    --> %s <-- " % [f]
                found = true
            end
        end
    end
    size = size - 1
end

